import Utils from '../Utils';

type ARGB = {
  a: number; // 0~255(0xFF)
  r: number; // 0~255(0xFF)
  g: number; // 0~255(0xFF)
  b: number; // 0~255(0xFF)
};

type RGB = {
  r: number; // 0~255(0xFF)
  g: number; // 0~255(0xFF)
  b: number; // 0~255(0xFF)
};

export default class Color {
  readonly value: number;

  constructor(value: number) {
    this.value = value ?? 0x00000000;
  }

  static fromARGB(argb: ARGB): Color {
    let a = Utils.clamp(argb.a, {
      min: 0, max: 255
    });
    let r = Utils.clamp(argb.r, {
      min: 0, max: 255
    });
    let g = Utils.clamp(argb.g, {
      min: 0, max: 255
    });
    let b = Utils.clamp(argb.b, {
      min: 0, max: 255
    });
    return new Color((a << 24) | (r << 16) | (g << 8) | b);
  }

  static fromRGB(rgb: RGB): Color {
    let r = Utils.clamp(rgb.r, {
      min: 0, max: 255
    });
    let g = Utils.clamp(rgb.g, {
      min: 0, max: 255
    });
    let b = Utils.clamp(rgb.b, {
      min: 0, max: 255
    });
    return new Color((0xFF << 24) | (r << 16) | (g << 8) | b);
  }

  formatToARGB(): string {

    function ff(v: number): string {
      let s = v.toString(16).toUpperCase();
      if (s.length == 1) {
        return `0${s}`
      } else {
        return s;
      }
    }

    return `#${ff(this.alpha)}${ff(this.red)}${ff(this.blue)}${ff(this.blue)}`
  }

  /**
   * 0~1
   */
  get opacity(): number {
    return this.alpha / 0xFF;
  }

  /**
   * 0~255(0xFF)
   */
  get alpha(): number {
    return this.value >>> 24;
  }

  /**
   * 0~255(0xFF)
   */
  get red(): number {
    return (this.value >> 16) & 0xFF;
  }

  /**
   * 0~255(0xFF)
   */
  get green(): number {
    return (this.value >> 8) & 0xFF;
  }

  /**
   * 0~255(0xFF)
   */
  get blue(): number {
    return (this.value) & 0xFF;
  }

  /**
   * 0~1
   */
  get luminance(): number {
    return ((0.2126 * this.red) + (0.7152 * this.green) + (0.0722 * this.blue)) / 255;
  }

  copyWith(argb: Partial<ARGB>): Color {
    return Color.fromARGB({
      a: argb.a ?? this.alpha,
      r: argb.r ?? this.red,
      g: argb.g ?? this.green,
      b: argb.b ?? this.blue,
    });
  }

  /**
   * 0~255(0xFF)
   */
  withAlpha(alpha: number): Color {
    return this.copyWith({
      a: alpha
    });
  }

  /**
   * 0~1
   */
  withOpacity(opacity: number): Color {
    return this.withAlpha(opacity * 0xFF);
  }

  blend(backColor: Color): Color {
    if (this.alpha <= 0) return backColor;
    if (this.alpha >= 0xFF) return this;
    let fa = this.opacity;
    let ba = backColor.opacity;
    return Color.fromARGB({
      a: (fa + ba - fa * ba) * 0xFF,
      r: this.red * fa + backColor.red * ba * (1 - fa),
      g: this.green * fa + backColor.green * ba * (1 - fa),
      b: this.blue * fa + backColor.blue * ba * (1 - fa),
    });
  }

  toHSL(): HSLColor {
    return new HSLColor(RGB2HSL({
      r: this.red,
      g: this.green,
      b: this.blue,
    }));
  }
}

type HSL = {
  h: number; // 0~1
  s: number; // 0~1
  l: number; // 0~1
};

export class HSLColor {
  /**
   * 0~1
   */
  readonly hue: number;

  /**
   * 0~1
   */
  readonly saturation: number;

  /**
   * 0~1
   */
  readonly lightness: number;

  constructor(hsl: HSL) {
    this.hue = Utils.clamp(hsl.h, { min: 0, max: 1 });
    this.saturation = Utils.clamp(hsl.s, { min: 0, max: 1 });
    this.lightness = Utils.clamp(hsl.l, { min: 0, max: 1 });
  }

  copyWith(hsl: Partial<HSL>): HSLColor {
    return new HSLColor({
      h: hsl.h ?? this.hue,
      s: hsl.s ?? this.saturation,
      l: hsl.l ?? this.lightness,
    });
  }

  toRGB(): Color {
    return Color.fromRGB(HSL2RGB({
      h: this.hue,
      s: this.saturation,
      l: this.lightness,
    }));
  }
}

/**
 * Converts an HSL color value to RGB. Conversion formula
 * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes h, s, and l are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 */
function HSL2RGB(hsl: HSL): RGB {
  const { h, s, l } = hsl;
  let r, g, b;

  if (s === 0) {
    r = g = b = l; // achromatic
  } else {
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;

    function HUE2RGB(p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    }

    r = HUE2RGB(p, q, h + 1 / 3);
    g = HUE2RGB(p, q, h);
    b = HUE2RGB(p, q, h - 1 / 3);
  }

  return {
    r: Math.min(Math.floor(r * 256), 255),
    g: Math.min(Math.floor(g * 256), 255),
    b: Math.min(Math.floor(b * 256), 255),
  };
}

/**
 * Converts an RGB color value to HSL. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes r, g, and b are contained in the set [0, 255] and
 * returns h, s, and l in the set [0, 1].
 */
function RGB2HSL(rgb: RGB): HSL {
  let { r, g, b } = rgb;
  (r /= 255), (g /= 255), (b /= 255);

  const max = Math.max(r, g, b), min = Math.min(r, g, b);
  let h, s, l = (max + min) / 2;

  if (max === min) {
    h = s = 0;
  } else {
    const d = max - min;
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
    if (max === g) h = (b - r) / d + 2;
    if (max === b) h = (r - g) / d + 4;
    h /= 6;
  }

  return { h: h, s: s, l: l };
}